package se.sll.itintegration.catalogue.product.service;

import java.util.*;

import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.inject.Inject;
import javax.validation.Validator;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;

import lombok.extern.slf4j.Slf4j;
import se.sll.itintegration.catalogue.product.ejb.CodeSystemEjb;
import se.sll.itintegration.catalogue.product.ejb.ProductEjb;
import se.sll.itintegration.catalogue.product.entity.CodeEntity;
import se.sll.itintegration.catalogue.product.entity.CodeSystemEntity;
import se.sll.itintegration.catalogue.product.entity.ProductEntity;
import se.sll.service.catalogue.common.exception.ResourceNotFoundException;
import se.sll.service.catalogue.common.exception.ServiceCatalogueException;
import se.sll.service.catalogue.common.exception.ServiceErrorCode;
import se.sll.service.catalogue.common.exception.ServiceException;
import se.sll.service.catalogue.common.exception.ValidationException;
import se.sll.service.catalogue.common.rest.request.CreateProductRequest;
import se.sll.service.catalogue.common.validation.FacadeRESTValidator;
import se.sll.service.catalogue.product.api.model.Code;
import se.sll.service.catalogue.product.api.model.ProductCreate;
import se.sll.service.catalogue.product.api.model.ProductList;
import se.sll.service.catalogue.product.api.model.Property;
import se.sll.service.catalogue.product.api.model.SourceSystemHSAId;

/**
 *
 * @author Jiri Uosukainen
 */
@Slf4j
@Stateless
public class ProductService implements se.sll.service.catalogue.product.api.resource.ProductService {

    @EJB
    private ProductEjb productEjb;
    @EJB
    private CodeSystemEjb codeSystemEjb;
    private static final String T_MEDIA_TYPE = "application/json; charset=UTF-8";
    private static final String T_HSA_OID = "";

    @Inject
    private Validator validator;
    private FacadeRESTValidator facadeRESTValidator = new FacadeRESTValidator();

    /**
     * For testing
     */
    void setProductEjb(ProductEjb productEjb) {
        this.productEjb = productEjb;
    }

    /**
     * For testing
     */
    void setCodeSystemEjb(CodeSystemEjb codeSystemEjb) {
        this.codeSystemEjb = codeSystemEjb;
    }

    /**
     * For testing
     */
    void setValidator(Validator validator) {
        this.validator = validator;
    }

    /**
     * For testing
     */
    void setFacadeRESTValidator(FacadeRESTValidator facadeRESTValidator) {
        this.facadeRESTValidator = facadeRESTValidator;
    }

    // CRUD + list operations
    //*********************************
    @Override
    public PostProductsResponse postProducts(ProductCreate entity) throws ServiceException {
        log.debug("ProductFacadeREST.create() was called: " + entity.toString());
        facadeRESTValidator.validate(entity, validator);
        if (entity.getName() != null) {
            List<ProductEntity> existingPs = productEjb.findByName(entity.getName());
            if (!existingPs.isEmpty()) {
                throw new ServiceCatalogueException(); // TODO Add error code
            }
        }
        ProductEntity p = new ProductEntity();
        if (entity.getName() == null) {
            entity.setName(productEjb.generateProductId());
        }
        try {
            Date now = new Date();
            p.setName(entity.getName());
            p.setDescription(entity.getDescription());
            p.setStatus(CreateProductRequest.STATUS.valueOf(entity.getStatus()));
            p.setDisplayName(entity.getDisplayName());
            p.setCreationTime(now);
            p.setUpdateTime(now);
            p.setUpdatedByHsaid(entity.getUpdatedByHsaid());
            p.setName(entity.getProductId());
            p.setParentProductId(entity.getParentProductId());
/**
            for (Property p : entity.getProperty()) {
                CodeEntity ce = new CodeEntity();
                ce.setCode(p.getCode());
                ce.setCodeSystem(p.getCodeSystem());
                p.setCodes(entity.getProperty());
            }
                */
            productEjb.create(p);

        } catch (RuntimeException e) {
            log.error("ProductFacadeREST.create() Caught RuntimeException: " + e.getMessage());
            throw new ServiceCatalogueException();
        }
        return PostProductsResponse.jsonCreated(transform(p, false));
    }

    @Override
    public PutProductsResponse putProducts(ProductCreate entity) throws ServiceException {
        log.debug("ProductFacadeREST.update() was called");

        facadeRESTValidator.validate(entity, validator);

        ProductEntity pe = productEjb.findByUniqueName(entity.getName());
        if (pe != null) {
            try {
                Date now = new Date();
                pe.setDescription(entity.getDescription());
                pe.setStatus(CreateProductRequest.STATUS.valueOf(entity.getStatus()));
                pe.setUpdatedByHsaid(entity.getUpdatedByHsaid());
                pe.setDisplayName(entity.getDisplayName());
                pe.setUpdateTime(now);
                productEjb.edit(pe);
            } catch (RuntimeException e) {
                log.error("ProductFacadeREST.update() Caught RuntimeException: " + e.getMessage());
                throw new ServiceCatalogueException();
            }
        } else {
            throw new ResourceNotFoundException(ServiceErrorCode.PRODUCT_NOT_FOUND_NAME, entity.getName());
        }
        return PutProductsResponse.jsonOK(transform(pe, false));
    }

    @Override
    public DeleteProductsByProductNameResponse deleteProductsByProductName(@PathParam("productName") String productName) throws ServiceException {
        log.debug("ProductFacadeREST.delete() was called");

        if (productName == null) {
            throw new ValidationException(ServiceErrorCode.INVALID_PRODUCT_NAME_EMPTY);
        }

        ProductEntity pe = productEjb.findByUniqueName(productName);

        if (pe != null) {
            productEjb.remove(pe);
        } else {
            throw new ResourceNotFoundException(ServiceErrorCode.PRODUCT_NOT_FOUND_NAME, productName);
        }

        return DeleteProductsByProductNameResponse.oK();
    }

    @Override
    public GetProductsByProductNameResponse getProductsByProductName(@PathParam("productName") String productName) throws ServiceException {
        log.debug("ProductFacadeREST.read() was called");

        ProductList product = null;

        try {
            ProductEntity p = productEjb.findByUniqueName(productName);
            product = transform(p, true);
        } catch (RuntimeException e) {
            log.error("ProductFacadeREST.read() Caught RuntimeException: Type: {}, Message: {} ", new Object[]{e.getClass().getName(), e.getMessage()});
            throw new ServiceCatalogueException();
        }

        if (product == null) {
            throw new ResourceNotFoundException(ServiceErrorCode.PRODUCT_NOT_FOUND_NAME, productName);
        }
        return GetProductsByProductNameResponse.jsonOK(product);
    }

    @Override
    public GetProductsByProductNameCodesResponse getProductsByProductNameCodes(
            @PathParam("productName") String productName) throws ServiceException {
        log.debug("ProductFacadeREST.readCodes() was called");
        List<Code> result = new ArrayList<>();

        if (productName == null) {
            throw new ValidationException(ServiceErrorCode.INVALID_PRODUCT_NAME_EMPTY);
        }
        try {
            ProductEntity pe = productEjb.findByUniqueName(productName);
            if (pe == null) {
                throw new ResourceNotFoundException(ServiceErrorCode.PRODUCT_NOT_FOUND_NAME, productName);
            }
            List<CodeEntity> cList = new ArrayList<>(pe.getCodes());

            for (CodeEntity codeEntity : cList) {
                Code code = new Code();
                code.setCode(codeEntity.getCode());
                code.setDisplayName(codeEntity.getDisplayName());
                code.setDescription(codeEntity.getDescription());
                code.setOid(codeEntity.getCodeSystem().getOid());
                code.setCodeSystem(codeEntity.getCodeSystem().getName());
                result.add(code);
            }

        } catch (RuntimeException e) {
            log.error("ProductFacadeREST.read() Caught RuntimeException: " + e.getMessage());
            throw new ServiceCatalogueException();
        }
        return GetProductsByProductNameCodesResponse.jsonOK(result);
    }

    @Override
    public GetProductsResponse getProducts() throws ServiceException {
        log.debug("ProductFacadeREST.list() was called");
        List<ProductList> products = new ArrayList<>();
        try {
            List<ProductEntity> list = productEjb.findAll();
            for (ProductEntity productEntity : list) {
                products.add(transform(productEntity, true));
            }
        } catch (RuntimeException e) {
            log.error("ProductFacadeREST.list() Caught RuntimeException: Type: {}, Message: {} ", new Object[]{e.getClass().getName(), e.getMessage()});
            throw new ServiceCatalogueException();
        }
        return GetProductsResponse.jsonOK(products);
    }

    // Modifierande metoder av olika typer
    //************************************
    @Override
    public PutProductsByProductNameAddCodeResponse putProductsByProductNameAddCode(
            @PathParam("productName") String productName, Code entity) throws ServiceException {
        log.debug("ProductFacadeREST.addCode() was called");
        log.debug("productName = " + productName);
        log.debug("code = " + entity);

        if (productName == null) {
            throw new ValidationException(ServiceErrorCode.INVALID_PRODUCT_NAME_EMPTY);
        }

        ProductEntity pe = productEjb.findByUniqueName(productName);
        if (pe == null) {
            log.warn("Product not found, productName:{}. Aborting (NOT_FOUND)", new Object[]{productName});
            throw new ResourceNotFoundException(ServiceErrorCode.PRODUCTS_NOT_FOUND, productName);
        }
        log.debug("Found Product: " + pe.toString());

        try {
            if (entity != null) {
                facadeRESTValidator.validate(entity, validator);
                // Check if the code exists in DB
                CodeEntity ce = codeSystemEjb.findUniqueCode(entity.getOid(), entity.getCode());
                if (ce != null) {
                    if (!pe.getCodes().contains(ce)) {
                        pe.getCodes().add(ce);
                        productEjb.edit(pe);
                    } else {
                        log.warn("Code already present: ProductName:{}, Code: {}. ", new Object[]{pe.getName(), ce.getCode()});
                    }
                } else {
                    log.warn("Code not found Code: {}. Aborting (BAD_REQUEST)", new Object[]{entity});
                    throw new ResourceNotFoundException(ServiceErrorCode.CODE_NOT_FOUND_OID_CODE, entity.getOid(), entity.getCode());
                }
                log.debug("Found Code :" + ce.toString());
            }
        } catch (RuntimeException rex) {
            log.error(rex.getMessage(), rex);
            throw new ServiceCatalogueException();
        }

        return PutProductsByProductNameAddCodeResponse.oK();
    }

    @Override
    public PutProductsByProductNameCodesResponse putProductsByProductNameCodes(
            @PathParam("productName") String productName, List<Code> entity) throws ServiceException {
        log.debug("ProductFacadeREST.setCodes() was called");
        log.debug("productName = " + productName);
        log.debug("codes = " + entity);
        List<Code> codeList = new ArrayList<>();
        ProductEntity pe = productEjb.findByUniqueName(productName);
        if (pe == null) {
            throw new ServiceCatalogueException(); //TODO: Add message
        }
        log.debug("Found Product: " + pe.toString());

        try {
            Set<CodeEntity> codesToSet = new HashSet<>();

            if (entity != null && !entity.isEmpty()) {

                for (Code code : entity) {
                    facadeRESTValidator.validate(code, validator);

                    // Check if the code exists in DB
                    CodeEntity ce = codeSystemEjb.findUniqueCode(code.getOid(), code.getCode());
                    if (ce != null) {
                        codesToSet.add(ce);
                    } else {
                        throw new ResourceNotFoundException(ServiceErrorCode.CODE_NOT_FOUND_OID_CODE, code.getOid(), code.getCode());
                    }
                    log.debug("Found Code :" + ce.toString());
                }
            }
            pe.setCodes(codesToSet);
            productEjb.edit(pe);
        } catch (RuntimeException rex) {
            log.error(rex.getMessage(), rex);
            throw new ServiceCatalogueException();
        }
        // TODO: Wut? Why not just return empty list directly? Why return anything at all?
        return PutProductsByProductNameCodesResponse.jsonOK(codeList);
    }

    // Finders
    //*********************************
    @Override
    public GetProductsNamesResponse getProductsNames(@QueryParam("productName") String productName) throws ServiceException {

        List<String> productNames = Arrays.asList(productName.split(","));
        System.out.println("!!!getProductsNames called!");
        log.debug("ProductFacadeREST.findByProductNames() was called");
        if (productNames == null || productNames.isEmpty()) {
            log.error("Indata: productnames = " + productNames);
            throw new ValidationException(ServiceErrorCode.INVALID_PRODUCT_NAMES_EMPTY);
        }

        List<ProductList> products = new ArrayList<>();
        try {
            List<ProductEntity> list = productEjb.findByNames(productNames);
            for (ProductEntity productEntity : list) {
                products.add(transform(productEntity, true));
            }
        } catch (RuntimeException e) {
            log.error("ProductFacadeREST.findByProductNames() Caught RuntimeException: Type: {}, Message: {} ", new Object[]{e.getClass().getName(), e.getMessage()});
            throw new ServiceCatalogueException();
        }

        return GetProductsNamesResponse.jsonOK(products);
    }

    @Override
    public GetProductsNamesNamebeginswithResponse getProductsNamesNamebeginswith(
            @QueryParam("productName") String productName) throws ServiceException {
        log.debug("ProductFacadeREST.nameBeginsWith() was called");

        if (productName == null || productName.isEmpty()) {
            log.debug("ProductFacadeREST.nameBeginsWith(): BAD_REQUEST");
            throw new ValidationException(ServiceErrorCode.INVALID_PRODUCT_NAME_EMPTY);
        }

        List<ProductList> products = new ArrayList<>();
        try {

            List<ProductEntity> list = productEjb.findByProductNameBeginningWith(productName);
            for (ProductEntity productEntity : list) {
                products.add(transform(productEntity, true));
            }

        } catch (RuntimeException e) {
            log.error("ProductFacadeREST.nameBeginsWith() Caught RuntimeException: Type: {}, Message: {} ", new Object[]{e.getClass().getName(), e.getMessage()});
            throw new ServiceCatalogueException();
        }
        return GetProductsNamesNamebeginswithResponse.jsonOK(products);
    }

    @Override
    public GetProductsNamecontainsResponse getProductsNamecontains(@QueryParam("productName") String productName) throws ServiceException {
        log.debug("ProductFacadeREST.nameContains() was called");

        if (productName == null || "".equals(productName)) {
            log.error("ProductFacadeREST.nameContains(): BAD_REQUEST");
            throw new ValidationException(ServiceErrorCode.INVALID_PRODUCT_NAME_EMPTY);
        }

        List<ProductList> products = new ArrayList<>();
        try {

            List<ProductEntity> list = productEjb.findByProductNameContains(productName);
            for (ProductEntity productEntity : list) {
                products.add(transform(productEntity, true));
            }
        } catch (RuntimeException e) {
            log.error("ProductFacadeREST.nameContains() Caught RuntimeException: Type: {}, Message: {} ", new Object[]{e.getClass().getName(), e.getMessage()});
            throw new ServiceCatalogueException();
        }

        return GetProductsNamecontainsResponse.jsonOK(products);
    }

    @Override
    public GetProductsCodesResponse getProductsCodes(@QueryParam("codeName") String codeName) throws ServiceException {
        log.debug("ProductFacadeREST.findByCode() was called");
        if (codeName == null || codeName.isEmpty()) {
            log.error("ProductFacadeREST.findByCode(): BAD_REQUEST");
            throw new ValidationException(ServiceErrorCode.INVALID_CODE_EMPTY);
        }
        List<ProductList> products = new ArrayList<>();
        try {
            List<ProductEntity> list = productEjb.findByCode(codeName);
            for (ProductEntity productEntity : list) {
                products.add(transform(productEntity, false));
            }
        } catch (RuntimeException e) {
            log.error("ProductFacadeREST.findByCode() Caught RuntimeException: Type: {}, Message: {} ", new Object[]{e.getClass().getName(), e.getMessage()});
            throw new ServiceCatalogueException();
        }
        return GetProductsCodesResponse.jsonOK(products);
    }

    @Override
    public GetProductsCodesystemsByCodeSystemCodesByCodeNameResponse getProductsCodesystemsByCodeSystemCodesByCodeName(
            @PathParam("codeSystem") String codeSystem,
            @PathParam("codeName") String codeName) throws ServiceException {
        log.debug("ProductFacadeREST.findByCodeSystemAndCode() was called");
        if (codeSystem == null || codeSystem.isEmpty()) {
            log.error("ProductFacadeREST.findByCodeSystemAndCode(): BAD_REQUEST");
            throw new ValidationException(ServiceErrorCode.INVALID_OID_EMPTY);
        }
        if (codeName == null || codeName.isEmpty()) {
            log.error("ProductFacadeREST.findByCodeSystemAndCode(): BAD_REQUEST");
            throw new ValidationException(ServiceErrorCode.INVALID_CODE_EMPTY);
        }

        List<ProductList> products = new ArrayList<>();

        try {
            List<ProductEntity> list = productEjb.findByOidAndCode(codeSystem, codeName);
            for (ProductEntity productEntity : list) {
                products.add(transform(productEntity, false));
            }
        } catch (RuntimeException e) {
            log.error("ProductFacadeREST.findByCodeSystemAndCode() Caught RuntimeException: Type: {}, Message: {} ", new Object[]{e.getClass().getName(), e.getMessage()});
            throw new ServiceCatalogueException();
        }
        return GetProductsCodesystemsByCodeSystemCodesByCodeNameResponse.jsonOK(products);
    }

    private ProductList transform(ProductEntity productEntity, boolean includeCodes) {
        log.debug("ProductFacadeREST.transform(ProductEntity) ENTER");

        ProductList prod = null;
        if (productEntity != null) {
            prod = new ProductList();
            prod.setDescription(productEntity.getDescription());
            prod.setDisplayName(productEntity.getDisplayName());
            prod.setName(productEntity.getName());
            prod.setStatus(productEntity.getStatus().name());
            SourceSystemHSAId hsaId = new SourceSystemHSAId();
            hsaId.setRoot(T_HSA_OID);
            hsaId.setExtension(productEntity.getUpdatedByHsaid());
                    
            prod.setSourceSystemHSAId(hsaId);
//            prod.setProperty(productEntity.getProperty);
//            prod.setRequestComment(productEntity.getRequestComment());

            prod.setCreationTime(productEntity.getCreationTime());
            prod.setUpdateTime(productEntity.getUpdateTime());
            prod.setUpdatedByHsaid(productEntity.getUpdatedByHsaid());

            List<Code> codeList = new ArrayList<>();
            prod.setCodes(codeList);
            //TODO Decide if we should return the codes also.
            if (includeCodes) {
                for (CodeEntity codeEntity : productEntity.getCodes()) {
                    Code code = transform(codeEntity);
                    if (code != null) {
                        codeList.add(code);
                    }
                }
            }
        }
        log.debug("ProductFacadeREST.transform(ProductEntity) EXIT");
        return prod;
    }

    private Code transform(CodeEntity codeEntity) {
        log.debug("ProductFacadeREST.transform(CodeEntity) ENTER");
        Code code = null;
        if (codeEntity != null) {
            code = new Code();
            CodeSystemEntity codeSystemEntity = codeEntity.getCodeSystem();
            if (codeSystemEntity != null) {
                code.setCodeSystem(codeSystemEntity.getName());
                code.setOid(codeSystemEntity.getOid());
            }
            code.setCode(codeEntity.getCode());
            code.setDisplayName(codeEntity.getDisplayName());
            code.setDescription(codeEntity.getDescription());
        }
        log.debug("ProductFacadeREST.transform(CodeEntity) EXIT");
        return code;
    }
}
